type WsLogger = {
  info: (msg: string) => void;
  warn?: (msg: string) => void;
  error?: (msg: string) => void;
};

interface WsOption {
  /** 是否开启断线重连, 默认: true */
  reconnection?: boolean;
  /** 最大重连次数, -1 则一直重连, 默认: -1 */
  maxRetries?: number;
  /** 断线时，缓存的发送的消息列表大小, -1 - 无限大小, 默认: 0 */
  maxCacheSize?: number;
  /** {@link https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/WebSocket#protocols 协议数组} */
  protocols?: string | string[];
  /** 配置自定义的日志记录, 默认: console */
  logger?: WsLogger;
  target?: WebSocket;
  /** 心跳检测间隔时间, 默认: 300s */
  heartInterval?: number;
}

const timer =
  globalThis.requestAnimationFrame ||
  ((cb: () => void) => {
    return setTimeout(() => {
      cb();
    }, 1000 / 60) as unknown as number;
  });

const cancelTimer = globalThis.cancelAnimationFrame || clearTimeout;

type WsCloseEvent = (e: CloseEvent) => void;
type WsEvent = () => void;
type WsMessageEvent = (data: string | Blob | ArrayBuffer) => void;

interface WsEventMap {
  close: WsCloseEvent;
  error: WsEvent;
  message: WsMessageEvent;
  open: WsEvent;
}

type EventsKey = keyof WsEventMap;
type EventsValue = WsEventMap[EventsKey];

function isJsonObject(obj: unknown) {
  return (
    typeof obj === "object" &&
    obj !== null &&
    (Array.isArray(obj) ||
      Object.prototype.toString.call(obj) === "[object Object]")
  );
}

export default class Ws {
  public url: string;
  private _option: Required<WsOption>;
  private _socket?: WebSocket;
  private _events: {
    open: Set<EventsValue>;
    close: Set<EventsValue>;
    error: Set<EventsValue>;
    message: Set<EventsValue>;
  };
  /** 重连次数 */
  private _reconnectCount: number;
  private _needReconnect: boolean;
  private cachedMessage?: Array<string | Blob | ArrayBuffer>;
  private _lastHeartTime: number;
  private _heartInterval: number;
  // biome-ignore lint:
  private Client: any;

  /** 定时重连 */
  private _reconnectInterval: number;
  private _lastReconnectTime?: number;

  public constructor(
    url: string,

    option?: WsOption
  ) {
    this._option = {
      reconnection: true,
      maxRetries: 5,
      maxCacheSize: 0,
      logger: console,
      heartInterval: 300,
      ...option,
    } as Required<WsOption>;
    this.Client = this._option.target || globalThis.WebSocket;
    this._option.target = undefined as unknown as WebSocket;
    this.url = url;
    this._needReconnect = this._option.reconnection;
    this._reconnectCount = 0;
    this._reconnectInterval = -1;
    this._events = {
      open: new Set(),
      close: new Set(),
      error: new Set(),
      message: new Set(),
    };

    this._lastHeartTime = 0;
    this._heartInterval = -1;
    this.connect();
  }

  private _onOpen = () => {
    setTimeout(() => {
      if (this.isOpen()) {
        this._log(`WebSocket connection established: ${this.url}`);
        this.emit("open");
        this._resetReconnect();
      }
    }, 0);
    if (this.cachedMessage != null) {
      // 复制缓存数据
      const cached1 = [...this.cachedMessage];
      const cached2 = []; // 新的发送失败的消息
      for (let i = 0, len = cached1.length; i < len; i++) {
        const msg = cached1[i];
        if (!this.send(msg, false)) {
          cached2.push(msg);
        }
      }
      this.cachedMessage = [...cached2, ...this.cachedMessage];
    }
    // 心跳检测
    this._heartDetec();
  };

  private _onClose = (e: CloseEvent) => {
    const isRe =
      (this._needReconnect &&
        (this._socket as WebSocket).readyState === WebSocket.CLOSED) ||
      (this._socket as WebSocket).readyState === WebSocket.OPEN;
    this._log(`websocket has close: ${e.code} - ${e.reason}`, "warn");
    this._dispose();
    if (isRe) {
      this.reconnect();
    } else {
      this._log(`WebSocket connection closed: ${this.url}`);
      this.emit("close", e);
    }
  };

  private _onMessage = (msg: MessageEvent) => {
    const data = msg.data;
    if (data === "PING") {
      this._onPong();
    } else {
      this.emit("message", msg.data);
    }
  };

  public on<K extends keyof WsEventMap>(type: K, handler: WsEventMap[K]) {
    if (this._events != null) {
      this._events[type as "open"].add(handler);
      if (
        type === "open" &&
        this._socket != null &&
        this._socket.readyState === this._socket.OPEN
      ) {
        (handler as WsEventMap["open"])();
      }
    }
  }

  public off<K extends keyof WsEventMap>(type: K, handler: WsEventMap[K]) {
    if (this._events != null) {
      this._events[type as "open"].delete(handler);
    }
  }

  public emit(type: EventsKey, param?: unknown) {
    if (this._events != null) {
      for (const handler of this._events[type as "open"]) {
        (handler as WsEventMap["message"])(param as string);
      }
    }
  }

  /** 连接到 Websocket */
  public connect() {
    if (
      this._socket == null ||
      this._socket.readyState === this.Client.CLOSED
    ) {
      this._needReconnect = this._option.reconnection;
      if (this._reconnectCount !== 0) {
        this._log(`The ${this._reconnectCount} reconnecting!`, "warn");
      }
      this._socket = new this.Client(this.url, this._option.protocols);
      const onName =
        "addEventListener" in (this._socket as WebSocket)
          ? "addEventListener"
          : "on";
      // @ts-ignore
      this._socket[onName]("open", this._onOpen);
      // @ts-ignore
      this._socket[onName]("close", this._onClose);
      // @ts-ignore
      this._socket[onName]("message", this._onMessage);
      // @ts-ignore
      this._socket[onName]("error", this._onError);
      // @ts-ignore
      this._socket[onName]("ping", this._onPing);
      // @ts-ignore
      this._socket[onName]("pong", this._onPong);
    }
  }

  private _dispose() {
    if (this._socket != null) {
      const offName =
        "removeEventListener" in this._socket ? "removeEventListener" : "off";
      // @ts-ignore
      this._socket[offName]("open", this._onOpen);
      // @ts-ignore
      this._socket[offName]("close", this._onClose);
      // @ts-ignore
      this._socket[offName]("message", this._onMessage);
      // @ts-ignore
      this._socket[offName]("error", this._onError);
      // @ts-ignore
      this._socket[offName]("ping", this._onPing);
      // @ts-ignore
      this._socket[offName]("pong", this._onPong);
      this._socket.close();
      this._socket = undefined as unknown as WebSocket;
    }
  }

  private _reconnectCallback() {
    if (
      this._option.maxRetries === -1 ||
      this._reconnectCount < this._option.maxRetries
    ) {
      const thisTime = Date.now();
      const timeDuration = Math.min(this._reconnectCount * 1000, 1800000);
      if (this._lastReconnectTime == null) {
        this._lastReconnectTime = Date.now();
      }
      if (thisTime - (this._lastReconnectTime as number) >= timeDuration) {
        this._lastReconnectTime = thisTime;
        this._reconnectCount++;
        this.connect();
      } else {
        this._reconnectInterval = timer(this._reconnectCallback.bind(this));
      }
    } else {
      this._log(
        `websocket ${this.url} reconnect over mas retries, stop reconnect`,
        "warn"
      );
    }
  }

  public reconnect() {
    this._dispose();
    if (this._option.reconnection) {
      cancelTimer(this._reconnectInterval);
      this._reconnectInterval = timer(this._reconnectCallback.bind(this));
    }
  }

  private _resetReconnect() {
    this._reconnectCount = 0;
    cancelTimer(this._reconnectInterval);
    this._lastReconnectTime = undefined;
  }

  public _onError() {
    this._log("connect error, please check network", "error");
    this.reconnect();
    this.emit("error");
  }

  /**
   * 断开连接
   *
   * @param [code] - {@link https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/close#code 关闭代码值}
   * @param [reason] - {@link https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/close#reason 关闭原因}
   * @param [destroy=true] - 是否在关闭时彻底释放资源, 彻底释放后, 则不能通过 connect 重连, 默认: true
   */
  public close(
    code?: number | undefined,
    reason?: string | undefined,
    destroy = true
  ) {
    cancelTimer(this._reconnectInterval);
    cancelTimer(this._heartInterval);
    this._needReconnect = false;
    if (this._socket != null) {
      this._socket.close(code, reason);
    }
    if (destroy) {
      if (this._events) {
        for (const key in this._events) {
          this._events[key as "open"].clear();
        }
        // biome-ignore lint:
        this._events = undefined as any;
      }
      if (this.cachedMessage != null) {
        this.cachedMessage = undefined;
      }
    }
  }

  public send(data: string | object | Blob | ArrayBuffer, isCache = false) {
    let sendData = data;
    if (isJsonObject(sendData)) {
      sendData = JSON.stringify(sendData);
    }
    if (this._socket && this._socket.readyState === this.Client.OPEN) {
      this._socket.send(sendData as string);
      return true;
    }
    if (isCache && this._option.maxCacheSize !== 0) {
      if (this.cachedMessage == null) {
        this.cachedMessage = [];
      }
      if (this.cachedMessage.length <= this._option.maxCacheSize) {
        this.cachedMessage.push(sendData as string);
      }
    }
    return false;
  }

  public ping() {
    if (this.isOpen()) {
      this._log("PING", "debug");
      // biome-ignore lint:
      if ((this._socket as any).ping != null) {
        // biome-ignore lint:
        (this._socket as any).ping();
      } else {
        //@ts-ignore
        this._socket.send("PING");
      }
    }
  }

  public pong() {
    if (this.isOpen()) {
      this._log("PONG", "debug");
      // biome-ignore lint:
      if ((this._socket as any).pong != null) {
        // biome-ignore lint:
        (this._socket as any).pong();
      } else {
        //@ts-ignore
        this._socket.send("PONG");
      }
    }
  }

  public isOpen() {
    return this._socket != null && this._socket.readyState === this.Client.OPEN;
  }

  public getState() {
    if (this._socket != null) {
      return this._socket.readyState;
    }
    return WebSocket.CLOSED;
  }

  private _log(msg: string, level = "info") {
    if (this._option.logger) {
      //@ts-ignore
      if (typeof this._option.logger[level] === "function") {
        //@ts-ignore
        this._option.logger[level](msg);
      } else {
        this._option.logger.info(msg);
      }
    }
  }

  private _handleHeart = () => {
    const _thisTime = Date.now();
    if (_thisTime - this._lastHeartTime >= this._option.heartInterval * 1000) {
      this._lastHeartTime = _thisTime;
      this.ping();
    }
    this._heartInterval = timer(this._handleHeart);
  };

  private _heartDetec() {
    if (this._option.heartInterval > 0) {
      cancelTimer(this._heartInterval);
      this._heartInterval = timer(this._handleHeart);
    }
  }

  private _onPing = () => {
    this._log("message PING", "debug");
    this.pong();
  };

  private _onPong = () => {
    this._log("message PONG", "debug");
  };
}
